﻿//    TestGenerator - experiment with piping data into ffmpeg in "pipefmt"
//    Copyright (C) 2012  Juergen Bohl

//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.

//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.

//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System.Security;

namespace TestGenerator
{
    #region Using directives
    using System;
    using System.IO;
    using System.Linq;
    using System.Runtime.InteropServices;
    using Microsoft.Win32;
    #endregion

    /// <summary>
    /// The class encapsulates all operation required to bring up ffmpeg,
    /// create a named pipe into it and then feeding images (=BitmapSources) into
    /// it.
    /// </summary>
    internal partial class FfmpegDriver : IAVSink, IDisposable
    {
        private string fileNameFfmpeg;

        private FfmpegAVPipe avPipe;

        private FfmpegProcess process;

        private FfmpegDriverInitOptions initOptions;

        /// <summary>
        /// Finalizes an instance of the <see cref="FfmpegDriver"/> class.
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="FfmpegDriver"/> is reclaimed by garbage collection.
        /// </summary>
        ~FfmpegDriver()
        {
            this.Dispose(false);
        }

        /// <summary>
        /// Gets the (fully qualified) filename of the ffmpeg executable.
        /// </summary>
        /// <value>
        /// The fully qualified filename of the ffmpeg executable.
        /// </value>
        public string FileNameFfmpeg
        {
            get { return this.fileNameFfmpeg; }
            set { this.fileNameFfmpeg = value; }
        }

        public void Initialize(FfmpegDriverInitOptions initOptions)
        {
            this.EnsureFileNameFfmpeg();

            VideoFormat vf = initOptions.VideoStreams != null ? initOptions.VideoStreams.FirstOrDefault() : null;
            AudioFormat af = initOptions.AudioStreams != null ? initOptions.AudioStreams.FirstOrDefault() : null;
            FfmpegAVPipe pipe = new FfmpegAVPipe();
            pipe.Init(vf, af);

            FfmpegProcess proc =
                new FfmpegProcess(
                    new FfmpegProcess.FfmpegProcessOptions(
                        this.FileNameFfmpeg,
                        pipe.PipeFileName,
                        initOptions.FileName,
                        initOptions.OverwriteExistingFile));

            proc.Start();

            // timeout of 10s
            pipe.WaitForConnection(10000);

            pipe.WriteHeader();

            this.avPipe = pipe;

            this.process = proc;
            this.initOptions = initOptions;
        }


        public void Close()
        {
            this.CheckIfInitialized();
            this.Dispose();
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Release managed resources:
                // Call Dispose() on other objects owned by this instance.
                //if (this.videoDataPipe != null)
                //{
                //    this.videoDataPipe.Dispose();
                //}

                if (this.process != null)
                {
                    this.process.Dispose();
                }
            }

            // Release unmanaged resources owned by (just) this instance.
        }

        private void EnsureFileNameFfmpeg()
        {
            if (!string.IsNullOrEmpty(this.FileNameFfmpeg))
            {
                return;
            }

            this.DetermineFfmpegPath();
        }

        /// <summary>
        /// Determines the full path to the ffmpeg-executable. If the executable is
        /// found, the full path is stored in the fileNameFfmpeg-property.
        /// </summary>
        private void DetermineFfmpegPath()
        {
            string startUpPath = Utility.GetStartupPath();
            string[] ffmpegNames = { "winffmpeg.exe", "ffmpeg.exe" };
            foreach (var ffmpegName in ffmpegNames)
            {
                string fullPathFfmpeg = Path.Combine(startUpPath, ffmpegName);
                if (File.Exists(fullPathFfmpeg))
                {
                    this.FileNameFfmpeg = fullPathFfmpeg;
                    return;
                }
            }

            // try to read the full path from registry
            // Open a subKey as read-only
            RegistryKey sk = Registry.CurrentUser.OpenSubKey(@"Software\JBO\TestGenerator");
            if (sk != null)
            {
                try
                {
                    // If the RegistryKey exists I get its value
                    // or null is returned.
                    string fullPathFfmpeg = sk.GetValue("FFMPEGEXE") as string;
                    if (!string.IsNullOrWhiteSpace(fullPathFfmpeg))
                    {
                        if (File.Exists(fullPathFfmpeg))
                        {
                            this.FileNameFfmpeg = fullPathFfmpeg;
                        }
                    }
                }
                catch (IOException)
                {
                }
                catch (SecurityException)
                {
                }
                catch (UnauthorizedAccessException)
                {
                }
            }
        }

        private void CheckIfInitialized()
        {
            //if (this.videoDataPipe == null)
            //{
            //    throw new InvalidOperationException("This class is not initialized.");
            //}
        }

        private byte[] ConvertToYv12Bitmap(IntPtr bitmapRgb24, int width, int height, int stride)
        {
            byte[] yv12Buffer = new byte[3 * width * height / 2];
            GCHandle pinnedDest = GCHandle.Alloc(yv12Buffer, GCHandleType.Pinned);
            BGRtoYV12.ConvertRgb24ToYv12(
                                    bitmapRgb24,
                                    stride,
                                    width,
                                    height,
                                    pinnedDest.AddrOfPinnedObject(),
                                    width,
                                    pinnedDest.AddrOfPinnedObject() + width * height,
                                    pinnedDest.AddrOfPinnedObject() + 5 * width * height / 4,
                                    width / 2);
            pinnedDest.Free();
            return yv12Buffer;
        }
    }

    /// <summary> Implementation of the methods of IAVSink.  </summary>
    internal partial class FfmpegDriver
    {
        /// <summary> Adds the specified frame (in pixel format rgb24). </summary>
        /// <exception cref="ArgumentException"> Thrown when one or more arguments have unsupported or
        /// illegal values. </exception>
        /// <param name="bitmap"> The bitmap (in pixel format RGB24). </param>
        /// <param name="size">   The size of the bitmap in bytes. </param>
        /// <param name="stride"> The stride. </param>
        public void AddFrameRgb24(IntPtr bitmap, int size, int stride)
        {
            VideoFormat vf = this.initOptions.VideoStreams.First();

            if (vf.Height * stride > size)
            {
                throw new ArgumentException("The size of the buffer is too small.");
            }

            byte[] buffer = this.ConvertToYv12Bitmap(bitmap, vf.Width, vf.Height, stride);

            this.avPipe.AddVideoFrame(buffer);
        }

        public void AddAudioSamplesInterleaved(short[] samples)
        {
            byte[] buffer = new byte[samples.Length * 2];
            Buffer.BlockCopy(samples, 0, buffer, 0, samples.Length * 2);
            this.avPipe.AddAudio(buffer);
        }
    }
}
